import { mount, shallowMount, type VueWrapper } from '@vue/test-utils'
import {
  defineComponent,
  reactive,
  ref,
  Transition,
  type ComponentPublicInstance,
  type CSSProperties,
} from 'vue'

const patchFlagExpect = (
  wrapper: VueWrapper<ComponentPublicInstance>,
  flag: number,
  dynamic: string[] | null,
) => {
  const { patchFlag, dynamicProps } = wrapper.vm.$.subTree as any

  expect(patchFlag).toBe(flag)
  expect(dynamicProps).toEqual(dynamic)
}

describe('Transform JSX', () => {
  test('should render with render function', () => {
    const wrapper = shallowMount({
      render() {
        return <div>123</div>
      },
    })
    expect(wrapper.text()).toBe('123')
  })

  test('should render with setup', () => {
    const wrapper = shallowMount({
      setup() {
        return () => <div>123</div>
      },
    })
    expect(wrapper.text()).toBe('123')
  })

  test('Extracts attrs', () => {
    const wrapper = shallowMount({
      setup() {
        return () => <div id="hi" />
      },
    })
    expect(wrapper.element.id).toBe('hi')
  })

  test('Binds attrs', () => {
    const id = 'foo'
    const wrapper = shallowMount({
      setup() {
        return () => <div>{id}</div>
      },
    })
    expect(wrapper.text()).toBe('foo')
  })

  test('should not fallthrough with inheritAttrs: false', () => {
    const Child = defineComponent({
      props: {
        foo: Number,
      },
      setup(props) {
        return () => <div>{props.foo}</div>
      },
    })

    Child.inheritAttrs = false

    const wrapper = mount({
      render() {
        return <Child class="parent" foo={1} />
      },
    })
    expect(wrapper.classes()).toStrictEqual([])
    expect(wrapper.text()).toBe('1')
  })

  test('Fragment', () => {
    const Child = () => <div>123</div>

    Child.inheritAttrs = false

    const wrapper = mount({
      setup() {
        return () => (
          <>
            <Child />
            <div>456</div>
          </>
        )
      },
    })

    expect(wrapper.html()).toBe('<div>123</div>\n<div>456</div>')
  })

  test('nested component', () => {
    const A = {
      B: defineComponent({
        setup() {
          return () => <div>123</div>
        },
      }),
    }

    A.B.inheritAttrs = false

    const wrapper = mount(() => <A.B />)

    expect(wrapper.html()).toBe('<div>123</div>')
  })

  test('xlink:href', () => {
    const wrapper = shallowMount({
      setup() {
        return () => <use xlinkHref={'#name'}></use>
      },
    })
    expect(wrapper.attributes()['xlink:href']).toBe('#name')
  })

  test('Merge class', () => {
    const wrapper = shallowMount({
      setup() {
        // @ts-expect-error
        return () => <div class="a" {...{ class: 'b' }} />
      },
    })
    expect(wrapper.classes().toSorted()).toEqual(['a', 'b'].toSorted())
  })

  test('Merge style', () => {
    const propsA = {
      style: {
        color: 'red',
      } satisfies CSSProperties,
    }
    const propsB = {
      style: {
        color: 'blue',
        width: '300px',
        height: '300px',
      } satisfies CSSProperties,
    }
    const wrapper = shallowMount({
      setup() {
        // @ts-ignore
        return () => <div {...propsA} {...propsB} />
      },
    })
    expect(wrapper.html()).toBe(
      '<div style="color: blue; width: 300px; height: 300px;"></div>',
    )
  })

  test('JSXSpreadChild', () => {
    const a = ['1', '2']
    const wrapper = shallowMount({
      setup() {
        return () => <div>{[...a]}</div>
      },
    })
    expect(wrapper.text()).toBe('12')
  })

  test('domProps input[value]', () => {
    const val = 'foo'
    const wrapper = shallowMount({
      setup() {
        return () => <input type="text" value={val} />
      },
    })
    expect(wrapper.html()).toBe('<input type="text" value="foo">')
  })

  test('domProps input[checked]', () => {
    const val = true
    const wrapper = shallowMount({
      setup() {
        return () => <input checked={val} />
      },
    })

    expect(wrapper.vm.$.subTree?.props?.checked).toBe(val)
  })

  test('domProps option[selected]', () => {
    const val = true
    const wrapper = shallowMount({
      render() {
        return <option selected={val} />
      },
    })
    expect(wrapper.vm.$.subTree?.props?.selected).toBe(val)
  })

  test('domProps video[muted]', () => {
    const val = true
    const wrapper = shallowMount({
      render() {
        return <video muted={val} />
      },
    })

    expect(wrapper.vm.$.subTree?.props?.muted).toBe(val)
  })

  test('Spread (single object expression)', () => {
    const props = {
      id: '1',
    }
    const wrapper = shallowMount({
      render() {
        return <div {...props}>123</div>
      },
    })
    expect(wrapper.html()).toBe('<div id="1">123</div>')
  })

  test('Spread (mixed)', async () => {
    const calls: number[] = []
    const data = {
      id: 'hehe',
      onClick() {
        calls.push(3)
      },
      innerHTML: '2',
      class: ['a', 'b'],
    }

    const wrapper = shallowMount({
      setup() {
        return () => (
          <button
            type="button"
            {...data}
            class={{ c: true }}
            onClick={() => calls.push(4)}
            hook-insert={() => calls.push(2)}
          />
        )
      },
    })

    expect(wrapper.attributes('id')).toBe('hehe')
    expect(wrapper.attributes('type')).toBe('button')
    expect(wrapper.text()).toBe('2')
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['a', 'b', 'c']))

    await wrapper.trigger('click')

    expect(calls).toEqual(expect.arrayContaining([3, 4]))
  })

  test('empty string', () => {
    const wrapper = shallowMount({
      setup() {
        return () => <h1 title=""></h1>
      },
    })
    expect(wrapper.html()).toBe('<h1 title=""></h1>')
  })
})

describe('directive', () => {
  test('vHtml', () => {
    const wrapper = shallowMount({
      setup() {
        const html = '<div>foo</div>'
        return () => <h1 v-html={html}></h1>
      },
    })
    expect(wrapper.html()).toBe('<h1>\n  <div>foo</div>\n</h1>')
  })

  test('vText', () => {
    const text = 'foo'
    const wrapper = shallowMount({
      setup() {
        return () => <div v-text={text}></div>
      },
    })
    expect(wrapper.html()).toBe('<div>foo</div>')
  })
})

describe('slots', () => {
  test('with default', () => {
    const A = defineComponent({
      setup(_, { slots }) {
        return () => (
          <div>
            {slots.default?.()}
            {slots.foo?.('val')}
          </div>
        )
      },
    })

    A.inheritAttrs = false

    const wrapper = mount({
      setup() {
        return () => (
          <A v-slots={{ foo: (val: string) => val }}>
            <span>default</span>
          </A>
        )
      },
    })

    expect(wrapper.html()).toBe('<div><span>default</span>val</div>')
  })

  test('without default', () => {
    const A = defineComponent({
      setup(_, { slots }) {
        return () => <div>{slots.foo?.('foo')}</div>
      },
    })

    A.inheritAttrs = false

    const wrapper = mount({
      setup() {
        return () => <A v-slots={{ foo: (val: string) => val }} />
      },
    })

    expect(wrapper.html()).toBe('<div>foo</div>')
  })
})

describe('PatchFlags', () => {
  test('static', () => {
    const wrapper = shallowMount({
      setup() {
        return () => <div class="static">static</div>
      },
    })
    patchFlagExpect(wrapper, 0, null)
  })

  test('props', async () => {
    const wrapper = mount({
      setup() {
        const visible = ref(true)
        const onClick = () => {
          visible.value = false
        }
        return () => (
          <div v-show={visible.value} onClick={onClick}>
            NEED_PATCH
          </div>
        )
      },
    })

    patchFlagExpect(wrapper, 8, ['onClick'])
    await wrapper.trigger('click')
    expect(wrapper.html()).toBe('<div style="display: none;">NEED_PATCH</div>')
  })

  test('#728: template literals with expressions should be treated as dynamic', async () => {
    const wrapper = mount({
      setup() {
        const foo = ref(0)
        return () => (
          <button
            value={String(foo.value)}
            onClick={() => foo.value++}
          ></button>
        )
      },
    })
    patchFlagExpect(wrapper, 8, ['value', 'onClick'])
    await wrapper.trigger('click')
    expect(wrapper.html()).toBe('<button value="1"></button>')
  })

  test('full props', async () => {
    const wrapper = mount({
      setup() {
        const bindProps = reactive({ class: 'a', style: { marginTop: 10 } })
        const onClick = () => {
          bindProps.class = 'b'
        }

        return () => (
          <div {...bindProps} class="static" onClick={onClick}>
            full props
          </div>
        )
      },
    })
    patchFlagExpect(wrapper, 16, ['onClick'])

    await wrapper.trigger('click')

    expect(wrapper.classes().toSorted()).toEqual(['b', 'static'].toSorted())
  })
})

describe('variables outside slots', () => {
  const A = defineComponent({
    props: {
      inc: Function,
    },
    render() {
      return this.$slots.default?.()
    },
  })

  A.inheritAttrs = false

  test('internal', async () => {
    const wrapper = mount(
      defineComponent({
        data() {
          return {
            val: 0,
          }
        },
        methods: {
          inc() {
            this.val += 1
          },
        },
        render() {
          const attrs = {
            innerHTML: String(this.val),
          }
          return (
            <A inc={this.inc}>
              <div>
                <Transition name="foo">
                  <textarea id="textarea" {...attrs} />
                </Transition>
              </div>
              <button id="button" onClick={this.inc}>
                +1
              </button>
            </A>
          )
        },
      }),
    )

    expect(wrapper.get('#textarea').element.innerHTML).toBe('0')
    await wrapper.get('#button').trigger('click')
    expect(wrapper.get('#textarea').element.innerHTML).toBe('1')
  })

  test('forwarded', async () => {
    const wrapper = mount(
      defineComponent({
        data() {
          return {
            val: 0,
          }
        },
        methods: {
          inc() {
            this.val += 1
          },
        },
        render() {
          const attrs = {
            innerHTML: String(this.val),
          }
          const textarea = <textarea id="textarea" {...attrs} />
          return (
            <A inc={this.inc}>
              <div>{textarea}</div>
              <button id="button" onClick={this.inc}>
                +1
              </button>
            </A>
          )
        },
      }),
    )

    expect(wrapper.get('#textarea').element.innerHTML).toBe('0')
    await wrapper.get('#button').trigger('click')
    expect(wrapper.get('#textarea').element.innerHTML).toBe('1')
  })
})

test('reassign variable as component should work', () => {
  let a: any = 1

  const A = defineComponent({
    setup(_, { slots }) {
      return () => <span>{slots.default!()}</span>
    },
  })

  const _a2 = 2
  a = _a2
  a = <A>{a}</A>

  const wrapper = mount({
    render() {
      return a
    },
  })

  expect(wrapper.html()).toBe('<span>2</span>')
})

describe('should support passing object slots via JSX children', () => {
  const A = defineComponent({
    setup(_, { slots }) {
      return () => (
        <span>
          {slots.default?.()}
          {slots.foo?.()}
        </span>
      )
    },
  })

  test('single expression, variable', () => {
    const slots = { default: () => 1, foo: () => 2 }

    const wrapper = mount({
      render() {
        return <A>{slots}</A>
      },
    })

    expect(wrapper.html()).toBe('<span>12</span>')
  })

  test('single expression, object literal', () => {
    const wrapper = mount({
      render() {
        return <A>{{ default: () => 1, foo: () => 2 }}</A>
      },
    })

    expect(wrapper.html()).toBe('<span>12</span>')
  })

  test('single expression, object literal', () => {
    const wrapper = mount({
      render() {
        return <A>{{ default: () => 1, foo: () => 2 }}</A>
      },
    })

    expect(wrapper.html()).toBe('<span>12</span>')
  })

  test('single expression, non-literal value', () => {
    const foo = () => 1

    const wrapper = mount({
      render() {
        return <A>{foo()}</A>
      },
    })

    expect(wrapper.html()).toBe('<span>1<!----></span>')
  })

  test('single expression, function expression', () => {
    const wrapper = mount({
      render() {
        return <A>{() => 'foo'}</A>
      },
    })

    expect(wrapper.html()).toBe('<span>foo<!----></span>')
  })

  test('single expression, function expression variable', () => {
    const foo = () => 'foo'

    const wrapper = mount({
      render() {
        return <A>{foo}</A>
      },
    })

    expect(wrapper.html()).toBe('<span>foo<!----></span>')
  })

  test('single expression, array map expression', () => {
    const data = ['A', 'B', 'C']

    const wrapper = mount({
      render() {
        return (
          <>
            {data.map((item) => (
              <A>
                <span>{item}</span>
              </A>
            ))}
          </>
        )
      },
    })

    expect(wrapper.html()).toMatchInlineSnapshot(
      `
      "<span><span>A</span>
      <!----></span>
      <span><span>B</span>
      <!----></span>
      <span><span>C</span>
      <!----></span>"
    `,
    )
  })

  test('xx', () => {
    const data = ['A', 'B', 'C']

    const wrapper = mount({
      render() {
        return (
          <>
            {data.map((item) => (
              <A>{() => <span>{item}</span>}</A>
            ))}
          </>
        )
      },
    })

    expect(wrapper.html()).toMatchInlineSnapshot(
      `
      "<span><span>A</span>
      <!----></span>
      <span><span>B</span>
      <!----></span>
      <span><span>C</span>
      <!----></span>"
    `,
    )
  })
})
